查看原文
其他

Android 中的卡顿丢帧原因概述 - 低内存篇

Gracker Android Performance 2021-11-05

在Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中 , 实际案例这里我们有列举一些由于系统低内存导致的卡顿 , 由于 Android 低内存对整机性能影响比较大 , 所以单独写一篇文章 , 来概述系统低内存对整机性能的影响 .

随着 Android 系统版本的更迭 , 以及 App 的代码膨胀 , Android 系统对内存的需求越来越大 , 但是目前市面上还存在着大量的 4G 内存以下的机器 , 这部分用户就很容易遇到整机低内存的情况 , 尤其是在系统大版本更新和 App 越装越多的情况下 .

Android 低内存会导致性能问题 , 具体表现就是响应慢和卡顿 . 比如启动一个应用要花比平时更长的时间 ; 滑动列表会掉更多帧 ; 后台的进程减少导致冷启动变多 ; 手机很容易发热发烫等 , 下面我会概述发生这些性能问题的原因 . Debug 的方法 , 以及可能的优化措施 .

  1. Android 中的卡顿丢帧原因概述 - 方法论[1]
  2. Android 中的卡顿丢帧原因概述 - 系统篇[2]
  3. Android 中的卡顿丢帧原因概述 - 应用篇[3]
  4. Android 中的卡顿丢帧原因概述 - 低内存篇[4]

低内存的数据特征和行为特征

Meminfo 信息

最简单的方法是使用 Android 系统自带的 Dumpsys meminfo 工具

adb shell dumpsys meminfo......Total RAM: 7,658,060K (status moderate) Free RAM: 550,200K ( 78,760K cached pss + 156,ba480K cached kernel + 314,960K free) Used RAM: 7,718,091K (6,118,703K used pss + 1,599,388K kernel) Lost RAM: -319,863K ZRAM: 2,608K physical used for 301,256K in swap (4,247,544K total swap) Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)

如果系统处于低内存的话 , 会有如下特征:

  1. FreeRam 的值非常少 , Used RAM 的值非常大
  2. ZRAM 使用率非常高(如果开了 Zram 的话)

LMK && kswapd 线程活跃

低内存的时候, LKMD 会非常活跃, 在 Kernel Log 里面可以看到 LMK 杀进程的信息:

[kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906,to free 28864kB on behalf of 'kswapd0' (91) becausecache 258652kB is below limit 261272kB for oom score 906Free memory is -5540kB above reserved.Free CMA is 3172kBTotal reserve is 227288kBTotal free pages is 271748kBTotal file cache is 345384kBGFP mask is 0x14000c0

上面这段 Log 的意思是说, 由于 mem 低于我们设定的 900 的水位线 (261272kB),所以把 pid 为 15609 的 mzsyncservice 这个进程杀掉(这个进程的 adj 是 906 )

proc/meminfo

这里是 Linux Kernel 展示 meminfo 的地方 , 关于 meminfo 的解读,可以参考这篇文章:/PROC/MEMINFO 之谜[5]

从结果来 , 当系统处于低内存的情况时候 , MemFree 和 MemAvailable 的值都很小

MemTotal: 5630104 kBMemFree: 148928 kBMemAvailable: 864172 kBBuffers: 28464 kBCached: 1003144 kBSwapCached: 19844 kBActive: 1607512 kBInactive: 969208 kBActive(anon): 1187828 kBInactive(anon): 426192 kBActive(file): 419684 kBInactive(file): 543016 kBUnevictable: 62152 kBMlocked: 62152 kBSwapTotal: 2097148 kBSwapFree: 42576 kBDirty: 3604 kBWriteback: 0 kBAnonPages: 1602928 kBMapped: 996768 kBShmem: 7284 kBSlab: 306440 kBSReclaimable: 72320 kBSUnreclaim: 234120 kBKernelStack: 89776 kBPageTables: 107572 kBNFS_Unstable: 0 kBBounce: 0 kBWritebackTmp: 0 kBCommitLimit: 4912200 kBCommitted_AS: 118487976 kBVmallocTotal: 263061440 kBVmallocUsed: 0 kBVmallocChunk: 0 kBCmaTotal: 303104 kBCmaFree: 3924 kB

整机卡顿 && 响应慢

低内存的时候,整机使用的时候要比非低内存的时候要卡很多,点击应用或者启动 App 都会有不顺畅或者响应慢的感觉

低内存对性能的具体影响

影响主线程 IO 操作

主线程出现大量的 IO 相关的问题 ,

  1. 反馈到 Trace 上就是有大量的黄色 Trace State 出现 , 例如 : Uninterruptible Sleep | WakeKill - Block I/O .
  2. 查看其 Block 信息 (kernel callsite when blocked:: "wait_on_page_bit_killable+0x78/0x88)

Linux 系统的 page cache 链表中有时会出现一些还没准备好的 page ( 即还没把磁盘中的内容完全地读出来 ) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.

当出现大量的 IO 操作的时候,应用主线程的 Uninterruptible Sleep 也会变多,此时涉及到 io 操作(比如 view ,读文件,读配置文件、读 odex 文件),都会触发 Uninterruptible Sleep , 导致整个操作的时间变长

Uninterruptible Sleep - Block IO
Uninterruptible Sleep - Block IO

出现 CPU 竞争

低内存会触发 Low Memory Killer 进程频繁进行扫描和杀进程,kswapd0 是一个内核工作线程,内存不足时会被唤醒,做内存回收的工作。当内存频繁在低水位的时候,kswapd0 会被频繁唤醒,占用 cpu ,造成卡顿和耗电。

比如下面这个情况, kswapd0 占用了 855 的超大核 cpu7 ,而且是满频在跑,耗电可想而知,如果此时前台应用的主线程跑到了 cpu7 上,很大可能会出现 cpu 竞争,导致调度不到而丢帧。

kswapd0 占满大核

HeapTaskDaemon 通常也会在低内存的时候跑的很高,来做内存回收相关的操作

进程频繁查杀和重启

对 AMS 的影响主要集中在进程的查杀上面 , 由于 LMK 的介入 , 处于 Cache 状态的进程很容易被杀掉 , 然后又被他们的父进程或者其他的应用所拉起来 , 导致陷入了一种死循环 . 对系统 CPU \ Memory \ IO 等资源的影响非常大.

比如下面就是一次 Monkey 之后的结果 , QQ 在短时间内频繁被杀和重启 .

07-23 14:32:16.969 1435 3420 I am_proc_bound: [0,30387,com.tencent.mobileqq]07-23 14:32:16.979 1435 3420 I am_kill : [0,30387,com.tencent.mobileqq,901,empty #3]07-23 14:32:16.996 1435 3420 I am_proc_died: [0,30387,com.tencent.mobileqq,901,18]07-23 14:32:17.028 1435 1510 I am_proc_start: [0,30400,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.054 1435 3420 I am_proc_bound: [0,30400,com.tencent.mobileqq]07-23 14:32:17.064 1435 3420 I am_kill : [0,30400,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.082 1435 3420 I am_proc_died: [0,30400,com.tencent.mobileqq,901,18]07-23 14:32:17.114 1435 1510 I am_proc_start: [0,30413,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.139 1435 3420 I am_proc_bound: [0,30413,com.tencent.mobileqq]07-23 14:32:17.149 1435 3420 I am_kill : [0,30413,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.166 1435 3420 I am_proc_died: [0,30413,com.tencent.mobileqq,901,18]07-23 14:32:17.202 1435 1510 I am_proc_start: [0,30427,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.216 1435 3420 I am_proc_bound: [0,30427,com.tencent.mobileqq]07-23 14:32:17.226 1435 3420 I am_kill : [0,30427,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.249 1435 3420 I am_proc_died: [0,30427,com.tencent.mobileqq,901,18]07-23 14:32:17.278 1435 1510 I am_proc_start: [0,30440,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.299 1435 3420 I am_proc_bound: [0,30440,com.tencent.mobileqq]07-23 14:32:17.309 1435 3420 I am_kill : [0,30440,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.329 1435 2116 I am_proc_died: [0,30440,com.tencent.mobileqq,901,18]07-23 14:32:17.362 1435 1510 I am_proc_start: [0,30453,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.387 1435 2116 I am_proc_bound: [0,30453,com.tencent.mobileqq]07-23 14:32:17.398 1435 2116 I am_kill : [0,30453,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.420 1435 2116 I am_proc_died: [0,30453,com.tencent.mobileqq,901,18]07-23 14:32:17.447 1435 1510 I am_proc_start: [0,30466,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.474 1435 2116 I am_proc_bound: [0,30466,com.tencent.mobileqq]07-23 14:32:17.484 1435 2116 I am_kill : [0,30466,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.507 1435 2116 I am_proc_died: [0,30466,com.tencent.mobileqq,901,18]07-23 14:32:17.533 1435 1510 I am_proc_start: [0,30479,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.556 1435 2116 I am_proc_bound: [0,30479,com.tencent.mobileqq]07-23 14:32:17.566 1435 2116 I am_kill : [0,30479,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.587 1435 2116 I am_proc_died: [0,30479,com.tencent.mobileqq,901,18]07-23 14:32:17.613 1435 1510 I am_proc_start: [0,30492,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]07-23 14:32:17.636 1435 2116 I am_proc_bound: [0,30492,com.tencent.mobileqq]07-23 14:32:17.646 1435 2116 I am_kill : [0,30492,com.tencent.mobileqq,901,empty #3]07-23 14:32:17.667 1435 2116 I am_proc_died: [0,30492,com.tencent.mobileqq,901,18]

其对应的 Systrace - SystemServer 中可以看到 AM 在频繁杀 QQ 和起 QQ

QQ 频繁被杀和启动

此 Trace 对应的 CPU 部分也可以看到繁忙的 cpu

CPU 繁忙

影响内存分配和触发 IO

手机经过长时间老化使用整机卡顿一下 , 或者整体比刚刚开机的时候操作要慢 , 可能是因为触发了内存回收或者 block io , 而这两者又经常有关联 . 内存回收可能触发了 fast path 回收 \ kswapd 回收 \ direct reclaim 回收 \ LMK 杀进程回收等。(fast path 回收不进行回写)

回收的内容是匿名页 swapout 或者 file-backed 页写回和清空。(假设手机都是 swap file 都是内存,不是 disk), 涉及到 file 的,都可能操作 io,增加 block io 的概率。

还有更常见的是打开之前打开过的应用,没有第一次打开的快,需要加载或者卡一段时间 . 可能发生了 do_page_fault,这条路径经常见到 block io 在 wait_on_page_bit_killable(),如果是 swapout 内存,就要 swapin 了。如果是普通文件,就要 read out in pagecache/disk.

do_page_fault —> lock_page_or_retry -> wait_on_page_bit_killable 里面会判断 page 是否置位 PG_locked, 如果置位就一直阻塞, 直到 PG_locked 被清除 , 而 PG_locked 标志位是在回写开始时和 I/O 读完成时才会被清除,而 readahead 到 pagecache 功能也对 block io 产生影响,太大了增加阻塞概率。

实例

下面这个 Trace 是低内存情况下 ,抓取的一个 App 的冷启动 ,我们只取应用启动到第一帧显示的部分 ,总耗时为 2s 。可以看到其 Running 的总时间是 682 ms ,

低内存的启动情况

低内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 共花费了 2s . 从下面的 Thread 信息那里可以看到

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共花费 750 ms 左右(对比下面正常情况才 130 ms)
  2. Running 的时间在 600 ms (对比下面正常情况才 624 ms , 相差不大)
IO 操作阻塞主线程

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程也非常多 , 比如若干个 kworker 和 kswapd0.

高 IO 场景的 CPU 使用情况

正常内存情况下

正常内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 只需要 1.22s . 从下面的 Thread 信息那里可以看到

  1. Uninterruptible Sleep | WakeKill - Block I/O 和 Uninterruptible Sleep 这两栏总共才 130 ms.
  2. Running 的时间是 624 ms
正常情况启动

从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程非常少.

正常情况启动的 CPU 使用情况

可能的优化方案 (来自实际的经验和大佬分享的经验)

下面列举的只是一些经验之谈 , 具体问题还是得具体分析 , 在 Android 平台上 , 对三方应用的管控是非常重要的 , 很多小白用户 , 一大堆常驻通知和后台服务 , 导致这些 App 的优先级非常高 , 很难被杀掉 . 导致整机的内存长时间比较低 . 所以做系统的必要的优化之后 , 就要着重考虑对三方应用的查杀和管控逻辑 , 尽量减少后台进程的个数 , 在必要的时候 , 清理掉无用的进程来释放内存个前台应用使用.

  1. 提高 extra_free_kbytes 值
  2. 提高 disk I/O 读写速率,如用 UFS3.0,用固态硬盘
  3. 避免设置太大的 read_ahead_kb 值
  4. 使用 cgroup 的 blkio 来限制后台进程的 io 读操作,缩短前台 io 响应时间
  5. 提前做内存回收的操作,避免在用户使用应用时碰到而感受到稍微卡顿
  6. 增加 LMK 效率,避免无效的 kill
  7. kswapd 周期性回收更多的 high 水位
  8. 调整 swappiness 来平衡 pagecache 和 swap
  9. 策略 : 针对低内存机器做特殊的策略 , 比如杀进程更加激进 (这会带来用户体验的降低 , 所以这个度需要兼顾性能和用户体验)
  10. 策略 : 在内存不足的时候提醒用户(或者不提醒用户) , 杀掉不必要的后台进程 
  11. 策略 : 在内存严重不足且无法恢复的情况下 , 可以提示用户重启手机.

参考文章如下:

  1. android系统优化时 systrace 中wait_on_page_locked_killable阻塞过长[6]
  2. 线程被IO卡住的原因[7]

参考资料

[1]

Android 中的卡顿丢帧原因概述 - 方法论: https://www.androidperformance.com/2019/09/05/Android-Jank-Debug/

[2]

Android 中的卡顿丢帧原因概述 - 系统篇: https://www.androidperformance.com/2019/09/05/Android-Jank-Due-To-System/

[3]

Android 中的卡顿丢帧原因概述 - 应用篇: https://www.androidperformance.com/2019/09/05/Android-Jank-Due-To-App/

[4]

Android 中的卡顿丢帧原因概述 - 低内存篇: https://www.androidperformance.com/2019/09/18/Android-Jank-Due-To-Low-Memory/

[5]

/PROC/MEMINFO 之谜: http://linuxperf.com/?p=142

[6]

android系统优化时 systrace 中wait_on_page_locked_killable阻塞过长: https://blog.csdn.net/qkhhyga2016/article/details/79540119

[7]

线程被IO卡住的原因: https://blog.csdn.net/zsj100213/article/details/82427527



: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存